﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using iNet.Media.Audio;
using iNet.Tasks;

namespace iNet.Media.Video
{
	/// <summary>
	/// 影片格式轉換工作。
	/// </summary>
	public class VideoFormatConversionTask : MediaTask
	{
		#region 靜態欄位
		static readonly Regex _OutputDurationRegex = new Regex(@"^[\s]+Duration:[\s](?<Hours>[\d]+):(?<Minutes>[\d]+):(?<Seconds>[\d]+)\.(?<Milliseconds>[\d]+),[\s]start:[\s].+");
		static readonly Regex _OutputProgressRegex = new Regex(@"^frame=.+[\s]time=[\s]*(?<Hours>[\d]+):(?<Minutes>[\d]+):(?<Seconds>[\d]+)\.(?<Milliseconds>[\d]+)");
		static readonly IMediaFormat[] _SupportOutputAudioFormats = new IMediaFormat[]{
			AudioFormats.Aac,
		};
		static readonly IMediaFormat[] _SupportOutputContainerFormats = new IMediaFormat[]{
			ContainerFormats.Mpeg4,
		};
		static readonly IMediaFormat[] _SupportOutputVideoFormats = new IMediaFormat[]{
			VideoFormats.H264,
		};
		#endregion
		#region 欄位
		long? _ConversionDuration;
		MediaStorage _Destination;
		long? _EndPosition;
		bool _MaintainVideoAspectRatio = true;
		long? _OutputAudioBitRate;
		IMediaFormat _OutputAudioFormat = _SupportOutputAudioFormats[0];
		int? _OutputAudioSamplingRate;
		IMediaFormat _OutputContainerFormat = _SupportOutputContainerFormats[0];
		long? _OutputVideoBitRate;
		IMediaFormat _OutputVideoFormat = _SupportOutputVideoFormats[0];
		double? _OutputVideoFrameRate;
		Size<int>? _OutputVideoFrameSize;
		bool _OverwriteDestination;
		MediaStorage _Source;
		long? _StartPosition;
		#endregion
		#region 建構子
		/// <summary>
		/// 初始化新的 VideoFormatConversionTask 執行個體。
		/// </summary>
		public VideoFormatConversionTask()
		{ }
		#endregion
		#region ConvertFormat
		bool ConvertFormat(TaskVersion version, string ffmpegArgs, double maxProgress)
		{
			//取得狀態
			double baseProgress = this.Progress.GetValueOrDefault();
			double progressRange = (maxProgress - baseProgress);

			//準備 FFmpeg
			Process process = new Process();
			process.StartInfo.FileName = "cmd";
			process.StartInfo.Arguments = "/C ffmpeg " + ffmpegArgs;
			process.StartInfo.UseShellExecute = false;
			process.StartInfo.RedirectStandardInput = true;
			process.StartInfo.RedirectStandardError = true;
			process.StartInfo.RedirectStandardOutput = true;
			process.StartInfo.CreateNoWindow = true;
			process.StartInfo.ErrorDialog = false;
			process.ErrorDataReceived += (sender, e) =>
			{
				this.OnFFmpegOutputReceived(version, process, e.Data, baseProgress, progressRange);
			};
			process.OutputDataReceived += (sender, e) =>
			{
				this.OnFFmpegOutputReceived(version, process, e.Data, baseProgress, progressRange);
			};

			//啟動 FFmpeg
			try
			{
				if (!process.Start())
				{
					this.Logger.Error("ConvertFormat() - Fail to start FFmpeg.");
					return false;
				}
			}
			catch (Exception ex)
			{
				this.Logger.Error("ConvertFormat() - Fail to start FFmpeg.", ex);
				return false;
			}

			//轉換影片
			try
			{
				if (!this.DoAction(new Action(() =>
				{
					process.BeginErrorReadLine();
					process.BeginOutputReadLine();
					process.WaitForExit();
				}), System.Threading.Timeout.Infinite))
				{
					return false;
				}
			}
			finally
			{
				//關閉 FFmpeg
				try
				{
					if (!process.HasExited)
						process.Kill();
				}
				catch
				{ }
			}

			//完成
			return true;
		}
		#endregion
		#region Destination
		/// <summary>
		/// 取得或設定轉換後的目標影片儲存。
		/// </summary>
		public MediaStorage Destination
		{
			get
			{
				return _Destination;
			}
			set
			{
				this.SetInitializingParameter<MediaStorage>(ref _Destination, value);
			}
		}
		#endregion
		#region EndPosition
		/// <summary>
		/// 取得或設定結束轉換的影片位置，單位為毫秒。Null (Visual Basic 為 Nothing) 表示轉換至影片尾端。
		/// </summary>
		public long? EndPosition
		{
			get
			{
				return _EndPosition;
			}
			set
			{
				this.SetInitializingParameter<long?>(ref _EndPosition, value);
			}
		}
		#endregion
		#region Execute
		/// <summary>
		/// 執行工作。
		/// </summary>
		/// <param name="version">工作版本。</param>
		/// <returns>True 表示工作執行成功，False 表示執行失敗。</returns>
		protected override bool Execute(TaskVersion version)
		{
			//取得轉換參數
			MediaStorage source, destination;
			IMediaFormat audioFormat, videoFormat, containerFormat;
			bool overwrite;
			int? audioSamplingRate;
			long? audioBitRate, videoBitRate, startPosition, endPosition;
			double? videoFrameRate;
			Size<int>? videoFrameSize;
			if (!this.ReadValue<MediaStorage>(version, ref _Source, out source)
				|| !this.ReadValue<MediaStorage>(version, ref _Destination, out destination)
				|| !this.ReadValue<IMediaFormat>(version, ref _OutputAudioFormat, out audioFormat)
				|| !this.ReadValue<IMediaFormat>(version, ref _OutputVideoFormat, out videoFormat)
				|| !this.ReadValue<IMediaFormat>(version, ref _OutputContainerFormat, out containerFormat)
				|| !this.ReadValue<int?>(version, ref _OutputAudioSamplingRate, out audioSamplingRate)
				|| !this.ReadValue<long?>(version, ref _OutputAudioBitRate, out audioBitRate)
				|| !this.ReadValue<bool>(version, ref _OverwriteDestination, out overwrite)
				|| !this.ReadValue<long?>(version, ref _OutputVideoBitRate, out videoBitRate)
				|| !this.ReadValue<double?>(version, ref _OutputVideoFrameRate, out videoFrameRate)
				|| !this.ReadValue<Size<int>?>(version, ref _OutputVideoFrameSize, out videoFrameSize)
				|| !this.ReadValue<long?>(version, ref _StartPosition, out startPosition)
				|| !this.ReadValue<long?>(version, ref _EndPosition, out endPosition))
			{
				return true;
			}

			//取得來源檔案
			string srcFilePath;
			double progressRange = (source.StorageType == MediaStorageType.File ? 0 : 10);
			if (!this.GetMediaStorageFile(version, source, progressRange, out srcFilePath))
				return false;

			//取得目標檔案
			string destFilePath;
			bool isTempDestFile = (destination.StorageType != MediaStorageType.File);
			if (isTempDestFile)
				destFilePath = Path.GetTempFileName();
			else
				destFilePath = destination.FileName;

			//轉換影片格式
			try
			{
				//取得原始視訊大小
				Size<int> targetFrameSize = videoFrameSize.GetValueOrDefault();
				if (targetFrameSize.Width > 0 && targetFrameSize.Height > 0)
				{
					GenericVideoDecodeTask decodeTask = new GenericVideoDecodeTask(new MediaStorage(srcFilePath));
					decodeTask.MediaDecodeRequests = 0;
					decodeTask.VideoDecodeRequests = VideoDecodeRequests.DecodeVideoPixelSize;
					switch (this.ExecuteChildTask(version, decodeTask, false))
					{
						case TaskState.Executed:
							Size<int> originalFrameSize = decodeTask.DecodedVideoPixelSize.GetValueOrDefault();
							if (originalFrameSize.Width > 0 && originalFrameSize.Height > 0 && (originalFrameSize.Width != targetFrameSize.Width || originalFrameSize.Height != targetFrameSize.Height))
							{
								double scaleX = (targetFrameSize.Width / (double)originalFrameSize.Width);
								double scaleY = (targetFrameSize.Height / (double)originalFrameSize.Height);
								double scale = Math.Min(scaleX, scaleY);
								videoFrameSize = new Size<int>((int)(originalFrameSize.Width * scale), (int)(originalFrameSize.Height * scale));
							}
							break;
						case TaskState.Canceled:
							return true;
						default:
							return false;
					}
				}

				//開始準備參數
				StringBuilder ffmpegArgs = new StringBuilder();

				//準備起始時間參數
				if (startPosition > 0)
				{
					TimeSpan timeSpan = TimeSpan.FromMilliseconds(startPosition.Value);
					ffmpegArgs.AppendFormat("-ss {0}:{1}:{2}.{3} ", timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds, timeSpan.Milliseconds);
				}

				//準備輸入檔案參數
				ffmpegArgs.AppendFormat("-i \"{0}\"", srcFilePath);

				//準備複寫參數
				ffmpegArgs.Append(overwrite ? " -y" : " -n");

				//設定結束位置
				if (endPosition > 0)
				{
					TimeSpan timeSpan;
					if (startPosition == null || startPosition <= 0)
					{
						timeSpan = TimeSpan.FromMilliseconds(endPosition.Value);
						ffmpegArgs.AppendFormat(" -to {0}:{1}:{2}.{3}", timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds, timeSpan.Milliseconds);
					}
					else
					{
						timeSpan = TimeSpan.FromMilliseconds(endPosition.Value - startPosition.Value);
						ffmpegArgs.AppendFormat(" -t {0}:{1}:{2}.{3}", timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds, timeSpan.Milliseconds);
					}
					_ConversionDuration = (long)timeSpan.TotalMilliseconds;
				}

				//準備音訊參數
				if (!this.PrepareAudioCodecArgs(ffmpegArgs, audioFormat, audioSamplingRate, audioBitRate))
					return false;

				//準備視訊參數
				if (!this.PrepareVideoCodecArgs(ffmpegArgs, videoFormat, videoFrameSize, videoFrameRate, videoBitRate))
					return false;

				//準備容器參數
				if (!this.PrepareContainerFormatArgs(ffmpegArgs, containerFormat))
					return false;

				//準備輸出檔案參數
				ffmpegArgs.AppendFormat(" \"{0}\"", destFilePath);

				//轉換影片
				double maxProgress = (isTempDestFile ? 90 : 100);
				bool result = this.ConvertFormat(version, ffmpegArgs.ToString(), maxProgress);
				if (result)
				{
					try
					{
						if (!File.Exists(destFilePath) || (new FileInfo(destFilePath)).Length == 0 || this.IsCanceling)
						{
							File.Delete(destFilePath);
							result = false;
						}
					}
					catch (Exception ex)
					{
						this.Logger.Error("Execute() - Fail to check destination file.", ex);
						result = false;
					}
					this.ReportProgress(version, maxProgress);
				}

				//確認結果
				if (result)
				{
					if (isTempDestFile && !this.CommitToMediaStorage(version, destination, 10, destFilePath))
					{
						this.Logger.Error("Execute() - Fail to commit converted video to destination.");
						return false;
					}
				}
				else
					return false;
			}
			finally
			{
				//釋放來源檔案
				this.ReleaseMediaStorageFile(version, source, srcFilePath);

				//刪除暫存檔案
				if (isTempDestFile)
				{
					try
					{
						File.Delete(destFilePath);
					}
					catch
					{ }
				}
			}

			//完成
			this.ReportProgress(version, 100);
			return true;
		}
		#endregion
		#region ExtractMilliseconds
		long ExtractMilliseconds(Match match)
		{
			int hours = (match.Groups["Hours"].Success ? int.Parse(match.Groups["Hours"].Value) : 0);
			int minutes = (match.Groups["Minutes"].Success ? int.Parse(match.Groups["Minutes"].Value) : 0);
			int seconds = (match.Groups["Seconds"].Success ? int.Parse(match.Groups["Seconds"].Value) : 0);
			int millis = (match.Groups["Milliseconds"].Success ? int.Parse(match.Groups["Milliseconds"].Value) : 0);
			return ((hours * 3600000L) + (minutes * 60000L) + (seconds * 1000L) + millis);
		}
		#endregion
		#region GetAudioCodecName
		static string GetAudioCodecName(IMediaFormat format)
		{
			if (format == AudioFormats.Aac)
				return "aac";
			else if (format == null)
				return "copy";
			return null;
		}
		#endregion
		#region GetContainerFormatName
		static string GetContainerFormatName(IMediaFormat format)
		{
			if (format == ContainerFormats.Mpeg4)
				return "mp4";
			return null;
		}
		#endregion
		#region GetVideoCodecName
		static string GetVideoCodecName(IMediaFormat format)
		{
			if (format == VideoFormats.H264)
				return "h264";
			else if (format == null)
				return "copy";
			return null;
		}
		#endregion
		#region MaintainVideoAspectRatio
		/// <summary>
		/// 取得或設定是否要維持原始的視訊長寬比例，預設值為 True。
		/// </summary>
		public bool MaintainVideoAspectRatio
		{
			get
			{
				return _MaintainVideoAspectRatio;
			}
			set
			{
				this.SetInitializingParameter<bool>(ref _MaintainVideoAspectRatio, value);
			}
		}
		#endregion
		#region OnFFmpegOutputReceived
		void OnFFmpegOutputReceived(TaskVersion version, Process process, string str, double baseProgress, double progressRange)
		{
			try
			{
				//中斷確認
				if (this.IsCanceling || this.CurrentVersion != version)
				{
					this.StopFFmpeg(process);
					return;
				}

				//輸出值確認
				if (str == null)
					return;

				//確認影片長度
				Match match = _OutputDurationRegex.Match(str);
				if (match.Success)
				{
					if (_ConversionDuration == null)
					{
						long duration = this.ExtractMilliseconds(match);
						long startPosition = _StartPosition.GetValueOrDefault();
						if (startPosition >= 0)
							duration -= startPosition;
						if (duration > 0)
						{
							if (!this.WriteValue<long?>(version, ref _ConversionDuration, duration))
								this.StopFFmpeg(process);
						}
					}
					return;
				}

				//確認進度
				match = _OutputProgressRegex.Match(str);
				if (match.Success)
				{
					//取得影片時間
					long? duration;
					if (!this.ReadValue<long?>(version, ref _ConversionDuration, out duration))
					{
						this.StopFFmpeg(process);
						return;
					}

					//更新進度
					if (duration > 0)
					{
						long convertedDuration = Math.Min(this.ExtractMilliseconds(match), duration.Value);
						double progressRatio = Math.Min(1, convertedDuration / (double)duration.Value);
						this.ReportProgress(version, baseProgress + (progressRatio * progressRange));
					}
					return;
				}
			}
			catch (Exception ex)
			{
				this.Logger.Error("OnFFmpegOutputReceived() - Error handling output.", ex);
				this.StopFFmpeg(process);
			}
		}
		#endregion
		#region OutputAudioBitRate
		/// <summary>
		/// 取得或設定輸出的音訊位元率，Null (Visual Basic 為 Nothing) 表示使用原始或預設的位元率。
		/// </summary>
		public long? OutputAudioBitRate
		{
			get
			{
				return _OutputAudioBitRate;
			}
			set
			{
				this.SetInitializingParameter<long?>(ref _OutputAudioBitRate, value);
			}
		}
		#endregion
		#region OutputAudioFormat
		/// <summary>
		/// 取得或設定輸出的音訊格式，Null (Visual Basic 為 Nothing) 表示使用原始或預設的格式。
		/// </summary>
		public IMediaFormat OutputAudioFormat
		{
			get
			{
				return _OutputAudioFormat;
			}
			set
			{
				this.SetInitializingParameter<IMediaFormat>(ref _OutputAudioFormat, value);
			}
		}
		#endregion
		#region OutputAudioSamplingRate
		/// <summary>
		/// 取得或設定輸出的音訊取樣頻率，Null (Visual Basic 為 Nothing) 表示使用原始或預設的 (44100) 取樣頻率。
		/// </summary>
		public int? OutputAudioSamplingRate
		{
			get
			{
				return _OutputAudioSamplingRate;
			}
			set
			{
				this.SetInitializingParameter<int?>(ref _OutputAudioSamplingRate, value);
			}
		}
		#endregion
		#region OutputContainerFormat
		/// <summary>
		/// 取得或設定輸出的影音容器格式，Null (Visual Basic 為 Nothing) 表示使用原始的格式。
		/// </summary>
		public IMediaFormat OutputContainerFormat
		{
			get
			{
				return _OutputContainerFormat;
			}
			set
			{
				this.SetInitializingParameter<IMediaFormat>(ref _OutputContainerFormat, value);
			}
		}
		#endregion
		#region OutputVideoBitRate
		/// <summary>
		/// 取得或設定輸出的視訊位元率，Null (Visual Basic 為 Nothing) 表示使用原始或預設的位元率。
		/// </summary>
		public long? OutputVideoBitRate
		{
			get
			{
				return _OutputVideoBitRate;
			}
			set
			{
				this.SetInitializingParameter<long?>(ref _OutputVideoBitRate, value);
			}
		}
		#endregion
		#region OutputVideoFormat
		/// <summary>
		/// 取得或設定輸出的視訊格式，Null (Visual Basic 為 Nothing) 表示使用原始或預設的格式。
		/// </summary>
		public IMediaFormat OutputVideoFormat
		{
			get
			{
				return _OutputVideoFormat;
			}
			set
			{
				this.SetInitializingParameter<IMediaFormat>(ref _OutputVideoFormat, value);
			}
		}
		#endregion
		#region OutputVideoFramePixelSize
		/// <summary>
		/// 取得或設定輸出的畫格像素大小，Null (Visual Basic 為 Nothing) 表示使用原始的像素大小。
		/// </summary>
		public Size<int>? OutputVideoFramePixelSize
		{
			get
			{
				return _OutputVideoFrameSize;
			}
			set
			{
				this.SetInitializingParameter<Size<int>?>(ref _OutputVideoFrameSize, value);
			}
		}
		#endregion
		#region OutputVideoFrameRate
		/// <summary>
		/// 取得或設定輸出的畫格頻率 (Frames Per Second)，Null (Visual Basic 為 Nothing) 表示使用原始或預設的 (30) 畫格頻率。
		/// </summary>
		public double? OutputVideoFrameRate
		{
			get
			{
				return _OutputVideoFrameRate;
			}
			set
			{
				this.SetInitializingParameter<double?>(ref _OutputVideoFrameRate, value);
			}
		}
		#endregion
		#region OverwriteDestination
		/// <summary>
		/// 取得或設定是否要覆寫目標影片儲存，預設值為 False。
		/// </summary>
		public bool OverwriteDestination
		{
			get
			{
				return _OverwriteDestination;
			}
			set
			{
				this.SetInitializingParameter<bool>(ref _OverwriteDestination, value);
			}
		}
		#endregion
		#region PrepareAudioCodecArgs
		bool PrepareAudioCodecArgs(StringBuilder argsBuffer, IMediaFormat format, int? samplingRate, long? bitRate)
		{
			//取得音訊格式
			string formatName = GetAudioCodecName(format);
			if (formatName == null)
				return false;

			//設定音訊格式
			argsBuffer.Append(" -c:a " + formatName);

			//設定取樣頻率
			if (samplingRate == null && format == AudioFormats.Aac)
				samplingRate = 44100;
			if (samplingRate > 0)
				argsBuffer.Append(" -ar " + samplingRate.Value);

			//設定位元率
			if (bitRate > 0)
				argsBuffer.Append(" -b:a " + bitRate.Value);

			//開啟實驗功能
			if (format == AudioFormats.Aac)
				argsBuffer.Append(" -strict -2");

			//完成
			return true;
		}
		#endregion
		#region PrepareContainerFormatArgs
		bool PrepareContainerFormatArgs(StringBuilder argsBuffer, IMediaFormat format)
		{
			//設定格式
			string formatName = GetContainerFormatName(format);
			if (formatName == null)
				return false;
			argsBuffer.Append(" -f " + formatName);

			//完成
			return true;
		}
		#endregion
		#region PrepareVideoCodecArgs
		bool PrepareVideoCodecArgs(StringBuilder argsBuffer, IMediaFormat format, Size<int>? frameSize, double? frameRate, long? bitRate)
		{
			//設定格式
			string formatName = GetVideoCodecName(format);
			if (formatName == null)
				return false;
			argsBuffer.Append(" -c:v " + formatName);

			//設定畫面大小
			if (frameSize != null)
			{
				Size<int> size = frameSize.Value;
				if (size.Width > 0 && size.Height > 0)
					argsBuffer.AppendFormat(" -s {0}x{1}", size.Width, size.Height);
			}

			//設定 FPS
			if (frameRate > 30)
				argsBuffer.Append(" -r " + frameRate.Value);

			//設定位元率
			if (bitRate > 0)
				argsBuffer.Append(" -b:v " + bitRate.Value);

			//完成
			return true;
		}
		#endregion
		#region ResetOverride
		/// <summary>
		/// 重設工作狀態。
		/// </summary>
		protected override void ResetOverride()
		{
			//重設狀態
			_ConversionDuration = null;

			//呼叫基底
			base.ResetOverride();
		}
		#endregion
		#region Source
		/// <summary>
		/// 取得或設定來源的影片儲存。
		/// </summary>
		public MediaStorage Source
		{
			get
			{
				return _Source;
			}
			set
			{
				this.SetInitializingParameter<MediaStorage>(ref _Source, value);
			}
		}
		#endregion
		#region StartPosition
		/// <summary>
		/// 取得或設定開始轉換的影片位置，單位為毫秒。Null (Visual Basic 為 Nothing) 表示從影片開頭轉換。
		/// </summary>
		public long? StartPosition
		{
			get
			{
				return _StartPosition;
			}
			set
			{
				this.SetInitializingParameter<long?>(ref _StartPosition, value);
			}
		}
		#endregion
		#region StopFFmpeg
		void StopFFmpeg(Process process)
		{
			try
			{
				if (!process.HasExited)
					process.Kill();
			}
			catch
			{ }
		}
		#endregion
		#region SupportOutputAudioFormats
		/// <summary>
		/// 取得所有支援的音訊輸出格式。
		/// </summary>
		public static IMediaFormat[] SupportOutputAudioFormats
		{
			get
			{
				return _SupportOutputAudioFormats.Clone() as IMediaFormat[];
			}
		}
		#endregion
		#region SupportOutputContainerFormats
		/// <summary>
		/// 取得所有支援的影音容器輸出格式。
		/// </summary>
		public static IMediaFormat[] SupportOutputContainerFormats
		{
			get
			{
				return _SupportOutputContainerFormats.Clone() as IMediaFormat[];
			}
		}
		#endregion
		#region SupportOutputVideoFormats
		/// <summary>
		/// 取得所有支援的視訊輸出格式。
		/// </summary>
		public static IMediaFormat[] SupportOutputVideoFormats
		{
			get
			{
				return _SupportOutputVideoFormats.Clone() as IMediaFormat[];
			}
		}
		#endregion
		#region ValidateInitParameters
		/// <summary>
		/// 驗證初始化參數是否正確。
		/// </summary>
		/// <returns>True 表示所有初始化參數皆正確，False 表示不完全正確。</returns>
		protected override bool ValidateInitParameters()
		{
			//呼叫基底
			if (!base.ValidateInitParameters())
				return false;

			//確認輸入及輸出
			if (_Source == null || _Destination == null)
				return false;

			//確認格式
			if (_OutputAudioFormat != null && !_SupportOutputAudioFormats.Contains(_OutputAudioFormat))
				return false;
			if (_OutputVideoFormat != null && !_SupportOutputVideoFormats.Contains(_OutputVideoFormat))
				return false;
			if (_OutputContainerFormat != null && !_SupportOutputContainerFormats.Contains(_OutputContainerFormat))
				return false;

			//確認轉換時間
			if (_StartPosition < 0 || _EndPosition < 0 || _EndPosition <= _StartPosition)
				return false;

			//確認位元率
			if (_OutputAudioBitRate <= 0 || _OutputVideoBitRate <= 0)
				return false;

			//確認取樣頻率
			if (_OutputAudioSamplingRate <= 0 || _OutputVideoFrameRate <= 0)
				return false;

			//確認畫格大小
			if (_OutputVideoFrameSize != null)
			{
				Size<int> frameSize = _OutputVideoFrameSize.Value;
				if (frameSize.Width <= 0 || frameSize.Height <= 0)
					return false;
			}

			//完成
			return true;
		}
		#endregion
	}
}
